﻿//#define SERIALIZER_DEBUG

#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

namespace AI.Graph.Editor {


    public static class Serializer {

        /// <summary>
        /// Creates and add a new exposed parameter to the given controller.
        /// </summary>
        /// <returns>Returns the index of the new added parameter.</returns>
        public static int CreateExposedParameterToController (AIController controller, bool autoSave = true) {
            ExposedParameter parameter;
            int index = controller.CreateUniqueExposedParameter (out parameter);
            AssetDatabase.AddObjectToAsset (parameter, controller);

            #if SERIALIZER_DEBUG
            Debug.Log ("Exposed Parameter [" + parameter.name + "] at index [" + index + "] created");
            #endif

            if (autoSave) {
                Save ();
            }

            controller.isDirty = true;

            return index;
        }


        /// <summary>
        /// Remove the given parameter from the given controller.
        /// </summary>
        public static void RemoveExposedParameterFromController (AIController controller, ExposedParameter parameter, bool autoSave = true) {
            controller.RemoveExposedParameter (parameter);

            #if SERIALIZER_DEBUG
            Debug.Log ("Exposed Parameter [" + parameter.name + "] removed.");
            #endif

            Object.DestroyImmediate (parameter, true);

            if (autoSave) {
                Save ();
            }

            controller.isDirty = true;
        }


        /// <summary>
        /// Creates and add a new node parameter to the given controller.
        /// </summary>
        /// <returns>Returns the index of the new added parameter.</returns>
        public static int CreateNodeParameterToController (AIController controller, bool autoSave = true) {
            NodeParameter parameter;
            int index = controller.CreateUniqueNodeParameter (out parameter);
            AssetDatabase.AddObjectToAsset (parameter, controller);

            #if SERIALIZER_DEBUG
            Debug.Log ("Node Parameter [" + parameter.name + "] at index [" + index + "] created");
            #endif

            if (autoSave) {
                Save ();
            }

            controller.isDirty = true;

            return index;
        }


        /// <summary>
        /// Remove the given parameter from the given controller.
        /// </summary>
        public static void RemoveNodeParameterFromController (AIController controller, NodeParameter parameter, bool autoSave = true) {
            controller.RemoveNodeParameter (parameter);

            #if SERIALIZER_DEBUG
            Debug.Log ("Exposed Parameter [" + parameter.name + "] removed.");
            #endif

            Object.DestroyImmediate (parameter, true);

            if (autoSave) {
                Save ();
            }

            controller.isDirty = true;
        }


        /// <summary>
        /// Add the given node to the given controller.
        /// Created and add all PortReferences that are used by the given node.
        /// </summary>
        public static void AddNewNodeToController (AIController controller, Node nodeToAdd, bool autoSave = true) {
            Node[] nodeAssets = controller.GetSubAssetsOfType<Node> ();

            foreach (Node node in controller.nodes) {
                if (node == nodeToAdd) {
                    Debug.LogError ("The given node [" + nodeToAdd.name + "] already exists in the given controller [" + controller.name + "].");
                    return;
                }
            }

            foreach (Node nodeAsset in nodeAssets) {
                if (nodeAsset == nodeToAdd) {
                    Debug.LogWarning ("The given node [" + nodeToAdd.name + "] exists already as an asset in the given controller [" + controller.name + "]. The node will be restored but without any Node Restore Data.");
                    RestoreNodeToController (controller, nodeToAdd, null, autoSave);
                    return;
                }
            }

            #if SERIALIZER_DEBUG
            nodeToAdd.name = nodeToAdd.name + "[" + controller.nodes.Length + "]";
            #endif

            nodeToAdd.hideFlags = HideFlags.HideInHierarchy;

            controller.AddNode (nodeToAdd);
            nodeToAdd.controller = controller;

            if (!controller.isCopy) {
                AssetDatabase.AddObjectToAsset (nodeToAdd, controller);
            }

            #if SERIALIZER_DEBUG
            Debug.Log ("Node [" + nodeToAdd.name + " added to the given controller [" + controller.name + "]");
            #endif

            PortReferenceManager.CreateConnectionPorts (nodeToAdd);

            if (!controller.isCopy) {
                foreach (ScriptableObject scriptableObject in nodeToAdd.GetScriptableObjects ()) {
                    #if SERIALIZER_DEBUG
                Debug.Log (scriptableObject.GetType ().Name + " [" + scriptableObject.name + "] added to the given node [" + nodeToAdd.name + "]");
                    #endif
                    AssetDatabase.AddObjectToAsset (scriptableObject, nodeToAdd);
                }
            }

            if (autoSave) {
                Save ();
            }

            controller.isDirty = true;
        }


        /// <summary>
        /// Add the given node to the given controller.
        /// Can only be used, if the node was already in the given controller.
        /// </summary>
        public static void RestoreNodeToController (AIController controller, Node node, NodeRestoreData nodeRestoreData, bool autoSave = true) {
            if (!node.IsSubAssetOf (controller)) {
                Debug.LogError ("The node [" + node.name + "] was never in the given controller [" + controller.name + "]");
                return;
            }

            foreach (ScriptableObject scriptableObject in node.GetScriptableObjects ()) {
                if (!scriptableObject.IsSubAssetOf (controller)) {
                    Debug.LogError ("The node [" + node.name + "] that should re-add has ScriptableObjects that are no sub assets of the given controller [" + controller.name + "]");
                    return;
                }
            }

            controller.AddNode (node);
            node.controller = controller;


            if (nodeRestoreData != null) {
                if (nodeRestoreData.restoredNode == node) {
                    #if SERIALIZER_DEBUG
                    Debug.Log ("Restore exposed parameter");
                    #endif

                    foreach (NodeRestoreData.ExposedParameterRestoreData restoreData in nodeRestoreData.exposedParameters) {
                        if (restoreData.parameterReference.type == restoreData.type) {
                            restoreData.parameterReference.Bind (node, restoreData.parameterReference.type);
                        }
                        else {
                            #if SERIALIZER_DEBUG
                            Debug.Log ("The Exposed Parameter [" + restoreData.parameterReference.name + "] has now a different bound type now: [" + restoreData.parameterReference.type + "] before: [" + restoreData.type + "]");
                            #endif
                        }
                    }

                    #if SERIALIZER_DEBUG
                    Debug.Log ("Restore node parameter");
                    #endif

                    foreach (NodeRestoreData.NodeParameterRestoreData restoreData in nodeRestoreData.nodeParameters) {
                        if (restoreData.parameterReference.type == restoreData.type) {
                            restoreData.parameterReference.Bind (node, restoreData.parameterReference.type);
                        }
                        else {
                            #if SERIALIZER_DEBUG
                            Debug.Log ("The Node Parameter [" + restoreData.parameterReference.name + "] has now a different bound type now: [" + restoreData.parameterReference.type + "] before: [" + restoreData.type + "]");
                            #endif
                        }
                    }

                    #if SERIALIZER_DEBUG
                    Debug.Log ("Restore connections");
                    #endif

                    foreach (ConnectionPort fromPort in nodeRestoreData.ports) {
                        ConnectionPort[] lastConnectedPorts = nodeRestoreData.GetConnectedPorts (fromPort);

                        foreach (ConnectionPort toPort in lastConnectedPorts) {
                            ConnectPorts (fromPort, toPort, false);
                        }
                    }
                }
                else {
                    Debug.LogWarning ("The given NodeRestoreData is not compatible with the given node [" + node.name + "]");
                }
            }

            #if SERIALIZER_DEBUG
            Debug.Log ("Node [" + node.name + " added again to the given controller [" + controller.name + "]");
            #endif

            if (autoSave) {
                Save ();
            }

            controller.isDirty = true;
        }


        /// <summary>
        /// Remove the given node from the given controller.
        /// </summary>
        /// <param name="destroyImmediate">If it's set to true, the node and all of its scriptableObject-Assets will be removed immediate. A re-add </param>
        public static NodeRestoreData RemoveNodeFromController (AIController controller, Node node, bool destroyImmediate = false, bool autoSave = true) {
            NodeRestoreData nodeRestoreData = null;
            if (!destroyImmediate) {
                nodeRestoreData = CreateNodeRestoreData (node);
            }

            // unbind exposed parameters.
            foreach (ExposedParameter exposedParameter in controller.exposedParameters) {
                if (exposedParameter.ContainsBoundedNode (node)) {
                    exposedParameter.Unbind (node);

                    #if SERIALIZER_DEBUG
                    Debug.Log ("Unbind [" + node.name + " from the exposed parameter [" + exposedParameter.name + "]");
                    #endif
                }
            }

            // unbind node parameters.
            foreach (NodeParameter nodeParameter in controller.nodeParameters) {
                if (nodeParameter.ContainsBoundedNode (node)) {
                    nodeParameter.Unbind (node);

                    #if SERIALIZER_DEBUG
                    Debug.Log ("Unbind [" + node.name + " from the node parameter [" + nodeParameter.name + "]");
                    #endif
                }
            }

            // disconnect all ports
            Serializer.DisconnectAllPorts (node, false);

            // remove from controller.
            controller.RemoveNode (node);
            node.controller = null;

            #if SERIALIZER_DEBUG
            Debug.Log ("Node [" + node.name + "] removed from the controller [" + controller.name + "]");
            #endif


            if (destroyImmediate) {
                foreach (ScriptableObject scriptableObject in node.GetScriptableObjects ()) {
                    if (scriptableObject != null) {
                        #if SERIALIZER_DEBUG
                        Debug.Log (scriptableObject.GetType ().Name + " [" + scriptableObject.name + "] destroyed");
                        #endif

                        Object.DestroyImmediate (scriptableObject, true);
                    }
                    else {
                        #if SERIALIZER_DEBUG
                        Debug.Log (scriptableObject + " is null");
                        #endif

                        Object.DestroyImmediate (scriptableObject, true);
                    }
                }

                #if SERIALIZER_DEBUG
                Debug.Log ("Node [" + node.name + "] destroyed");
                #endif

                Object.DestroyImmediate (node, true);

                Save ();

                return null;
            }

            if (autoSave) {
                Save ();
            }

            controller.isDirty = true;

            return nodeRestoreData;
        }


        /// <summary>
        /// Remove all connections of the given node.
        /// </summary>
        public static void DisconnectAllPorts (Node node, bool autoSave = true) {
            #if SERIALIZER_DEBUG
            Debug.Log ("Disconnect all Ports from node [" + node.name + "]");
            #endif

            foreach (ConnectionPort port in node.ports) {
                Serializer.DisconnectAllPorts (port, false);
            }

            if (autoSave) {
                Save ();
            }

            if (node.controller) {
                node.controller.isDirty = true;
            }
        }


        /// <summary>
        /// Remove all transitions of the given port.
        /// </summary>
        public static void DisconnectAllPorts (ConnectionPort port, bool autoSave = true) {
            if (port != null) {
                port.DisconnectAll ();

                #if SERIALIZER_DEBUG
                Debug.Log ("Port [" + port.name + "] disconnected from all connections");
                #endif

                if (autoSave) {
                    Save ();
                }

                if (port.node.controller) {
                    port.node.controller.isDirty = true;
                }
            }
            else {
                #if SERIALIZER_DEBUG
                Debug.Log ("Port is null");
                #endif
            }
        }


        /// <summary>
        /// Remove the transition between the two given ports.
        /// </summary>
        public static void DisconnectPorts (ConnectionPort port1, ConnectionPort port2, bool autoSave = true) {
            port1.Disconnect (port2);

            #if SERIALIZER_DEBUG
            Debug.Log ("Port [" + port1.name + "] disconnected from Port [" + port2.name + "]");
            #endif

            if (autoSave) {
                Save ();
            }

            if (port1.node && port1.node.controller) {
                port1.node.controller.isDirty = true;
            }
            else if (port2.node && port2.node.controller) {
                port2.node.controller.isDirty = true;
            }
        }


        /// <summary>
        /// Create a transition between the two given ports.
        /// </summary>
        /// <returns>Returns true if it was a valid connections.</returns>
        public static bool ConnectPorts (ConnectionPort port1, ConnectionPort port2, bool autoSave = true) {
            string error;
            if (!port1.TryConnectTo (port2, out error)) {
                Debug.LogWarning (error);
                return false;
            }

            #if SERIALIZER_DEBUG
            Debug.Log ("Port [" + port1.name + "] connected to Port [" + port2.name + "]");
            #endif

            if (autoSave) {
                Save ();
            }

            if (port1.node && port1.node.controller) {
                port1.node.controller.isDirty = true;
            }
            else if (port2.node && port2.node.controller) {
                port2.node.controller.isDirty = true;
            }

            return true;
        }


        /// <summary>
        /// Creates a restore data object of the given node.
        /// </summary>
        public static NodeRestoreData CreateNodeRestoreData (Node node) {
            return NodeRestoreData.Create (node);
        }


        public static bool CleanupAll (bool withProgress = false) {
            AIController[] allController = AllControllerInProject ();

            #if SERIALIZER_DEBUG
            Debug.Log ("Cleanup " + allController.Length + " Controller");
            #endif

            if (withProgress) {
                EditorUtility.ClearProgressBar ();
            }

            for (int i = 0; i < allController.Length; i++) {
                AIController controller = allController[i];

                if (withProgress) {
                    if (EditorUtility.DisplayCancelableProgressBar ("Save AI Controllers", i + "/" + allController.Length + " " + controller.name, allController.Length / (i * 1F))) {
                        break;
                    }
                }

                try {
                    CleanupInternal (allController[i], false);
                }
                catch (Exception e) {
                    Debug.LogError (e.Message);
                    EditorUtility.ClearProgressBar ();
                    Save ();
                    return false;
                }
            }

            if (withProgress) {
                EditorUtility.ClearProgressBar ();
            }

            Save ();
            return true;
        }


        /// <summary>
        /// Cleanup the given controller.
        /// Removes and destroys unreferenced nodes and ports.
        /// </summary>
        public static void Cleanup (AIController controller) {
            CleanupInternal (controller);
        }


        static void CleanupInternal (AIController controller, bool autoSave = true) {
            #if SERIALIZER_DEBUG
            Debug.Log (">>>> Cleanup start <<<<");
            #endif

            Node[] nodeAssets = controller.GetSubAssetsOfType<Node> ();

            foreach (Node node in nodeAssets) {
                if (!controller.nodes.Contains (node)) {
                    #if SERIALIZER_DEBUG
                    Debug.Log ("Found unreferenced Node: [" + node.name + "]");
                    #endif

                    RemoveNodeFromController (controller, node, true, false);
                }
            }

            ConnectionPort[] portAssets = controller.GetSubAssetsOfType<ConnectionPort> ();

            foreach (ConnectionPort port in portAssets) {
                if (port.node == null) {
                    #if SERIALIZER_DEBUG
                    Debug.Log ("Found unreferenced Port: [" + port.name + "]");
                    #endif
                    Object.DestroyImmediate (port, true);
                }
            }

            #if SERIALIZER_DEBUG
            Debug.Log (">>>> Cleanup end <<<<");
            #endif

            EditorUtility.SetDirty (controller);

            if (autoSave) {
                Save ();
            }

            controller.isDirty = false;
        }


        public static AIController[] AllControllerInProject () {
            // Doesn't work. If finds only objects that are loaded or has a reference to any other Object that was loaded.
            // return Resources.FindObjectsOfTypeAll<AIController> ();

            string[] assetGUIDs = AssetDatabase.FindAssets ("t:AIController");
            AIController[] allController = new AIController[assetGUIDs.Length];

            for (int i = 0; i < assetGUIDs.Length; i++) {
                string assetPath = AssetDatabase.GUIDToAssetPath (assetGUIDs[i]);
                allController[i] = AssetDatabase.LoadAssetAtPath<AIController> (assetPath);
            }

            return allController;
        }


        [MenuItem ("AI System/Create/AI Controller")]
        static void CreateAIController () {
            AIController controller = CreateAssetAtSelectionPath<AIController> ();

            EntryState entryState = ScriptableObject.CreateInstance<EntryState> ();
            entryState.position = new Vector2 (0, 0);
            entryState.name = "Entry";

            AnyState anyState = ScriptableObject.CreateInstance<AnyState> ();
            anyState.position = new Vector2 (0, 200);
            anyState.name = "Any";

            controller.entryState = entryState;
            controller.anyState = anyState;

            #if SERIALIZER_DEBUG
            Debug.Log ("Controller [" + controller.name + "] created.");
            #endif

            AddNewNodeToController (controller, entryState, false);
            AddNewNodeToController (controller, anyState, false);

            Save ();
        }


        /// <summary>
        /// Save and refresh the AssetDatabase.
        /// </summary>
        public static void Save () {
            AssetDatabase.SaveAssets ();
            AssetDatabase.Refresh ();

            #if SERIALIZER_DEBUG
            Debug.Log ("AssetDatabase saved and refreshed.");
            #endif
        }


        /// <summary>
        /// Creates an Asset at the selected Path. If no path is selected the Unity Asset folder [Assets] will be used.
        /// </summary>
        static T CreateAssetAtSelectionPath<T> (bool autoSelect = true) where T : ScriptableObject {
            string path = AssetDatabase.GetAssetPath (Selection.activeObject);

            if (path == "") {
                path = "Assets";
            }
            else if (Path.GetExtension (path) != "") {
                path = path.Replace (Path.GetFileName (AssetDatabase.GetAssetPath (Selection.activeObject)), "");
            }

            string assetPathAndName = AssetDatabase.GenerateUniqueAssetPath (path + "/New " + typeof (T).Name + ".asset");

            return CreateAsset<T> (assetPathAndName, autoSelect);
        }


        static T CreateAsset<T> (string pathAndName, bool autoSelect = true) where T : ScriptableObject {
            T asset = ScriptableObject.CreateInstance<T> ();
            AssetDatabase.CreateAsset (asset, pathAndName);

            #if SERIALIZER_DEBUG
            Debug.Log (asset.GetType ().Name + " [" + pathAndName + "] created.");
            #endif

            Save ();
            EditorUtility.FocusProjectWindow ();

            if (autoSelect) {
                Selection.activeObject = asset;
            }

            return asset;
        }

    }


    public class NodeRestoreData {

        private readonly Node m_restoredNode;
        private readonly List<ExposedParameterRestoreData> m_exposedParameters;
        private readonly List<NodeParameterRestoreData> m_nodeParameters;
        private readonly Dictionary<ConnectionPort, ConnectionPort[]> m_connections;


        internal Node restoredNode {
            get { return m_restoredNode; }
        }

        internal ExposedParameterRestoreData[] exposedParameters {
            get { return m_exposedParameters.ToArray (); }
        }

        internal NodeParameterRestoreData[] nodeParameters {
            get { return m_nodeParameters.ToArray (); }
        }

        internal ConnectionPort[] ports {
            get { return m_connections.Keys.ToArray (); }
        }


        internal ConnectionPort[] GetConnectedPorts (ConnectionPort port) {
            return m_connections[port].ToArray ();
        }


        private NodeRestoreData (Node node) {
            m_restoredNode = node;
            m_connections = new Dictionary<ConnectionPort, ConnectionPort[]> ();
            m_exposedParameters = new List<ExposedParameterRestoreData> ();
            m_nodeParameters = new List<NodeParameterRestoreData> ();

            // save exposed parameters.
            foreach (ExposedParameter exposedParameter in node.controller.exposedParameters) {
                if (exposedParameter.ContainsBoundedNode (node)) {
                    Add (exposedParameter);
                }
            }

            #if SERIALIZER_DEBUG
            Debug.Log ("[" + m_exposedParameters.Count + "] Exposed Parameters saved from node [" + node.name + "]");
            #endif

            // save node parameters.
            foreach (NodeParameter nodeParameter in node.controller.nodeParameters) {
                if (nodeParameter.ContainsBoundedNode (node)) {
                    Add (nodeParameter);
                }
            }

            #if SERIALIZER_DEBUG
            Debug.Log ("[" + m_nodeParameters.Count + "] Node Parameters saved from node [" + node.name + "]");
            #endif

            // save all connections
            foreach (ConnectionPort port in node.ports) {
                Add (port);
            }
        }


        internal struct ExposedParameterRestoreData {

            public ExposedParameter parameterReference;
            public Type type;

        }

        internal struct NodeParameterRestoreData {

            public NodeParameter parameterReference;
            public Type type;

        }


        void Add (ExposedParameter parameter) {
            ExposedParameterRestoreData data = new ExposedParameterRestoreData {parameterReference = parameter, type = parameter.type};
            m_exposedParameters.Add (data);
        }


        void Add (NodeParameter parameter) {
            NodeParameterRestoreData data = new NodeParameterRestoreData {parameterReference = parameter, type = parameter.type};
            m_nodeParameters.Add (data);
        }


        void Add (ConnectionPort port) {
            if (m_connections.ContainsKey (port)) {
                Debug.LogWarning ("The port [" + port.name + "] already exists and the connected ports will be overridden");
            }

            m_connections.Add (port, port.connectedPorts);
        }


        public static NodeRestoreData Create (Node node) {
            if (node.controller == null) {
                Debug.LogError ("The given node [" + node.name + "] has no reference to any controller. Maybe it was removed before");
                return null;
            }

            return new NodeRestoreData (node);
        }

    }


}
#endif